package com.tianji.promotion.service.impl;

import cn.hutool.core.bean.copier.CopyOptions;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.api.dto.promotion.OrderCouponDTO;
import com.tianji.common.exceptions.DbException;
import com.tianji.common.utils.*;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.discount.Discount;
import com.tianji.promotion.discount.DiscountStrategy;
import com.tianji.promotion.entity.dto.UserCouponDTO;
import com.tianji.promotion.entity.po.Coupon;
import com.tianji.promotion.entity.po.CouponScope;
import com.tianji.promotion.entity.po.ExchangeCode;
import com.tianji.promotion.entity.po.UserCoupon;
import com.tianji.promotion.entity.query.UserCouponQuery;
import com.tianji.promotion.entity.vo.CouponVO;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.enums.UserCouponStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.tianji.promotion.utils.CodeUtil;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * </p>
 *
 * @author lemon
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserCouponWithRedissonAndMqServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;

    private final IExchangeCodeService exchangeCodeService;

    private final RedissonClient redissonClient;

    private final StringRedisTemplate stringRedisTemplate;

    private final RabbitMqHelper mqHelper;

    private final ICouponScopeService couponScopeService;

    private final Executor discountSolutionExecutor;

    // 这两个就是基于redis的lua脚本来进行异步领取优惠券的操作，就是一气呵成，那么这样的话我们连锁就不需要了
    private static final RedisScript<Long> RECEIVE_COUPON_SCRIPT;
    private static final RedisScript<String> EXCHANGE_COUPON_SCRIPT;

    static {
        RECEIVE_COUPON_SCRIPT = RedisScript.of(new ClassPathResource("lua/receive_coupon.lua"), Long.class);
        EXCHANGE_COUPON_SCRIPT = RedisScript.of(new ClassPathResource("lua/exchange_coupon.lua"), String.class);
    }

    /**
     * 领取优惠券
     *
     * @param couponId
     */
    @Override
    public void receiveCoupon(Long couponId) {
        String redissonKey = "lock:coupon:uid:" + couponId;
        // 底层是可重入锁
        RLock lock = redissonClient.getLock(redissonKey);
        boolean tryLock = lock.tryLock();
        if (!tryLock) {
            throw new BizIllegalException("操作太频繁，请稍后再试");
        }
        try {
            if (couponId == null) {
                throw new BadRequestException("优惠券id不能为空");
            }

            // 判断状态

            // 现在就从redis里面获取
//        Coupon coupon = couponMapper.selectById(couponId);
            Coupon coupon = queryCouponByCache(couponId);
            if (coupon == null) {
                throw new BadRequestException("优惠券不存在");
            }
            // 2.校验发放时间
            LocalDateTime now = LocalDateTime.now();
            if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
                throw new BadRequestException("优惠券发放已经结束或尚未开始");
            }
            // 3.校验库存
            if (coupon.getTotalNum() <= 0) {
                throw new BadRequestException("优惠券库存不足");
            }
            Long userId = UserContext.getUser();

            // 统计已经领取的数量
            String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId;
            // increment表示领取后的优惠券数量
            Long increment = stringRedisTemplate.opsForHash().increment(key, userId.toString(), 1);

            // 校验是否超过限领数量
            if (increment > coupon.getUserLimit()) {
                // 还要减去
                // 其实也没有必要删除，因此他不可能再去领取了
//              stringRedisTemplate.opsForHash().increment(key, userId.toString(), -1);
                throw new BadRequestException("超出领取数量");
            }

            // 修改库存 -1
            stringRedisTemplate.opsForHash().increment(PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId, "totalNum", -1);

            // TODO 给mq发送消息
            // 发送的消息为  userId, couponId
            UserCouponDTO userCouponDTO = new UserCouponDTO();
            userCouponDTO.setUserId(userId);
            userCouponDTO.setCouponId(couponId);
            // 发送消息
            mqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, userCouponDTO);



        /* 下面的放在mq里面去做
        加锁，让锁在事务之前，这样事务就能完整提交
        String key = "lock:coupon:uid:" + userId;
        // 底层是可重入锁
        RLock lock = redissonClient.getLock(key);
        try {
            // 如果你加时间，看门狗机制就会失效
            boolean tryLock = lock.tryLock();
            if (!tryLock) {
                throw new BizIllegalException("操作太频繁，请稍后再试");
            }
            IUserCouponService currentProxy = (IUserCouponService) AopContext.currentProxy();
            // 这个不传第三个表示不是兑换码兑换的，也就不用去修改兑换码的状态了
            currentProxy.checkAndCreateUserCoupon(coupon, userId, null);

        } finally {
            // redisson的话里面是有判断的
            // 并且是原子操作，不会出现异常的情况下，锁不会自动释放
            lock.unlock();
        }*/
        } finally {
            lock.unlock();
        }


    }

    /**
     * 从redis中获取优惠券信息(领取的开始和结束时间，发行的数量和限领数量)
     *
     * @param couponId
     * @return coupon
     */
    private Coupon queryCouponByCache(Long couponId) {
        String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId;
        ;
        // 注意是获取所有的字段
        // 他的底层是hgetall
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(key);

        // map转为bean
        return BeanUtils.mapToBean(entries, Coupon.class, false, CopyOptions.create());


    }

    /**
     * 我的优惠券列表-分页查询-用户端
     *
     * @param query
     * @return
     */
    @Override
    public PageDTO<CouponVO> queryMyCouponPage(UserCouponQuery query) {
        Long userId = UserContext.getUser();
        Page<UserCoupon> page = this.lambdaQuery().eq(UserCoupon::getUserId, userId).eq(UserCoupon::getStatus, query.getStatus()).page(query.toMpPage(new OrderItem("term_end_time", true)));
        List<UserCoupon> records = page.getRecords();
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page);
        }
        // 获取优惠券信息先得id
        Set<Long> couponIds = records.stream().map(UserCoupon::getCouponId).collect(Collectors.toSet());
        // 3.2.查询
        List<Coupon> coupons = couponMapper.selectBatchIds(couponIds);

        // 4.转换
        List<CouponVO> couponVOS = coupons.stream().map(uc -> {
            return BeanUtils.copyBean(uc, CouponVO.class);
        }).collect(Collectors.toList());


        return PageDTO.of(page, couponVOS);
    }

    /**
     * 兑换码兑换优惠券
     *
     * @param code
     */
    @Override
    public void exchangeCoupon(String code) {
        Long userId = UserContext.getUser();
        String key = "lock:coupon:exchange:uid:" + userId;
        RLock lock = redissonClient.getLock(key);
        boolean tryLock = lock.tryLock();
        if (!tryLock) {
            throw new BizIllegalException("操作太频繁，请稍后再试");
        }


        // 1.检验code
        if (StringUtils.isBlank(code)) {
            throw new BadRequestException("兑换码不能为空");
        }

        // 2.解析校验码
        // 自增id
        long serialNum = CodeUtil.parseCode(code);
        // 采用redis中 setbit key offset 1的方式来判断兑换码是否已经兑换
        // serialNum表示偏移量
        boolean result = exchangeCodeService.updateExchangeCodeMark(serialNum, true);

        // 3.判断兑换码是否已经兑换  采用
        if (result) {
            // 说明已经被兑换了
            throw new BizIllegalException("兑换码已经被兑换");
        }
        try {
            // 4.根据自增id查询兑换码信息
            // ExchangeCode exchangeCode = exchangeCodeService.getById(serialNum);
            //
            Long couponId = exchangeCodeService.exchangeTargetId(serialNum);
            if (couponId == null) {
                throw new BizIllegalException("兑换码不存在");
            }
            // 6.校验并生成用户券
            // 6.1.优惠券信息
            // 6.2.这个就是兑换码对应的优惠券
            Coupon coupon = couponMapper.selectById(couponId);
            if (coupon == null) {
                throw new BizIllegalException("优惠券不存在");
            }


            // 5.判断是否过期
            LocalDateTime now = LocalDateTime.now();
            if (now.isAfter(coupon.getIssueEndTime()) || now.isBefore(coupon.getIssueBeginTime())) {
                throw new BizIllegalException("优惠券活动未开始或已经结束");
            }


            // 5.校验每人限领数量
            // 5.1.查询领取数量
            String userCouponKey = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId;
            Long count = stringRedisTemplate.opsForHash().increment(userCouponKey, userId.toString(), 1);
            // 5.2.校验限领数量
            if (count > coupon.getUserLimit()) {
                // 还要减去
                // 其实也没有必要删除，因此他不可能再去领取了
                stringRedisTemplate.opsForHash().increment(key, userId.toString(), -1);
                throw new BadRequestException("超出领取数量");
            }

            // 发送mq消息
            UserCouponDTO r = new UserCouponDTO();
            r.setUserId(userId);
            r.setCouponId(couponId);
            r.setSerialNum((int) serialNum);

            /* 控制防止重复提交
            // 以及事务的控制
           String key = "lock:coupon:exchange:uid:" + userId;
            RLock lock = redissonClient.getLock(key);
            boolean tryLock = lock.tryLock();
            if (!tryLock) {
                throw new BizIllegalException("操作太频繁，请稍后再试");
            }
            IUserCouponService currentProxy = (IUserCouponService) AopContext.currentProxy();
            // 6.3.校验并生成用户券，更新兑换码状态
            currentProxy.checkAndCreateUserCoupon(coupon, userId, (int) serialNum);*/

        } catch (Exception e) {
            // 将兑换码状态进行重置
            // 采用redis中 setbit key offset 0的方式来判断兑换码是否已经兑换
            exchangeCodeService.updateExchangeCodeMark(serialNum, false);
            // 还要抛出异常
            throw e;
        }

    }


    /**
     * 校验并生成用户券
     *
     * @param coupon
     * @param userId
     * @param serialNum
     */
    @Transactional
    public void checkAndCreateUserCoupon(Coupon coupon, Long userId, Integer serialNum) {
//        synchronized (userId.toString().intern()) {
        // 4.校验每人限领数量
        // 4.1.统计当前用户对当前优惠券的已经领取的数量
        Integer count = this.lambdaQuery().eq(UserCoupon::getUserId, userId).eq(UserCoupon::getCouponId, coupon.getId()).count();
        // 4.2.校验限领数量
        if (count != null && count >= coupon.getUserLimit()) {
            throw new BadRequestException("超出领取数量");
        }
        // 5.更新优惠券的已经发放的数量 + 1
        couponMapper.incrIssueNum(coupon.getId());
        // 6.新增一个用户券
        saveUserCoupon(coupon, userId);

        // 7.更新兑换码状态
        if (serialNum != null) {
            // 从db里面修改
            exchangeCodeService.lambdaUpdate().set(ExchangeCode::getStatus, ExchangeCodeStatus.USED).set(ExchangeCode::getUserId, userId).eq(ExchangeCode::getId, serialNum).update();
        }
//        }

    }

    /**
     * 生成用户券-消费者
     *
     * @param dto
     */
    @Transactional
    @Override
    public void checkAndCreateUserCouponNew(UserCouponDTO dto) {
        // 从数据库获取优惠券信息
        Coupon coupon = couponMapper.selectById(dto.getCouponId());
        if (coupon == null) {
            return;
        }

        // 5.更新优惠券的已经发放的数量 + 1
        int num = couponMapper.incrIssueNum(dto.getCouponId());
        if (num == 0) {
            // 说明已经发放完了
            return;
        }
        // 6.新增一个用户券
        saveUserCoupon(coupon, dto.getUserId());

        // 7.更新兑换码状态  看是否传送了兑换码的id
        if (dto.getSerialNum() != null) {
            // 从db里面修改
            exchangeCodeService.lambdaUpdate().set(ExchangeCode::getStatus, ExchangeCodeStatus.USED).set(ExchangeCode::getUserId, dto.getUserId()).eq(ExchangeCode::getId, dto.getSerialNum()).update();
        }
//        }
    }

    /**
     * 查询我的优惠券可用方案--第三方接口
     *
     * @param orderCourses
     * @return
     */
    @Override
    public List<CouponDiscountDTO> findDiscountSolution(List<OrderCourseDTO> orderCourses) {
        // TODO 注意存入优惠券的打折，以及满减的价钱都是以分为单位

        // 1.查询我的所有可用优惠券
        // 因为我们需要根据userId来看这个用户有哪些优惠券，
        // 然后获得优惠券的信息，然后根据优惠券的信息来计算优惠券的折扣
        // 并且是可以使用的优惠券 status = 1
        Long userId = UserContext.getUser();
        //
        List<Coupon> coupons = this.getBaseMapper().queryMyCoupons(userId);
        if (CollUtils.isEmpty(coupons)) {
            return CollUtils.emptyList();
        }

        // 2.初筛  根据订单总价格是否达到优惠券的门槛
        // 2.1.计算订单总价
        int totalAmount = orderCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
        // 2.2.筛选可用券
        List<Coupon> availableCoupons = coupons.stream().filter(item -> DiscountStrategy.getDiscount(item.getDiscountType()).canUse(totalAmount, item)).collect(Collectors.toList());
        // 判空
        if (CollUtils.isEmpty(availableCoupons)) {
            return CollUtils.emptyList();
        }

        // 3.排列组合出所有方案
        // 3.1.细筛（找出每一个优惠券的可用的课程，判断课程总价是否达到优惠券的使用需求）
        // - 首先要基于优惠券的限定范围对课程筛选，找出可用课程。如果没有可用课程，则优惠券不可用。
        //- 然后对可用课程计算总价，判断是否达到优惠门槛，没有达到门槛则优惠券不可用
        Map<Coupon, List<OrderCourseDTO>> avaMap = findAvailableCoupons(availableCoupons, orderCourses);

        if (avaMap.isEmpty()) {
            return CollUtils.emptyList();
        }
        // 细筛后的优惠券
        availableCoupons = new ArrayList<>(avaMap.keySet());

        log.info("经过细筛只有的信息");

        // 3.2.排列组合
        List<List<Coupon>> solutions = PermuteUtil.permute(availableCoupons);
        log.debug("排列组合");
        for (Coupon availableCoupon : availableCoupons) {
            // 添加单卷到方案中
            // 因为回显的时候，需要将单卷也显示出来
            // 先判断是否已经存在了
            if (solutions.stream().noneMatch(item -> item.contains(availableCoupon))) {
                solutions.add(Collections.singletonList(availableCoupon));
            }

        }


        /**
         4.计算方案的优惠明细
         4.1.循环遍历方案
         List<CouponDiscountDTO> couponDiscountDTOS = new ArrayList<>();
         for (List<Coupon> solution : solutions) {
         4.2.计算方案的优惠明细
         CouponDiscountDTO discountDTO = calculateSolutionDiscount(avaMap, orderCourses, solution);
         4.3.将方案的优惠明细放入结果集中
         couponDiscountDTOS.add(discountDTO);
         }*/

        // 5.采用多线程的方式计算方案的优惠明细
        // 4.计算方案的优惠明细
        List<CouponDiscountDTO> list = Collections.synchronizedList(new ArrayList<>(solutions.size()));
        // 4.1.定义闭锁
        CountDownLatch latch = new CountDownLatch(solutions.size());
        for (List<Coupon> solution : solutions) {
            // 4.2.异步计算
            CompletableFuture.supplyAsync(
                    // 计算方案的优惠明细
                    () -> calculateSolutionDiscount(avaMap, orderCourses, solution), discountSolutionExecutor).thenAccept(dto -> {
                log.debug("方案最终优惠 {}, 方案中优惠券使用了 {}  规则{}", dto.getDiscountAmount(), dto.getIds(), dto.getRules());
                // 4.3.提交任务结果
                list.add(dto);
                latch.countDown();
            });
        }
        // 4.4.等待运算结束
        try {
            boolean await = latch.await(1, TimeUnit.SECONDS);
            System.out.println("await = " + await);
        } catch (InterruptedException e) {
            log.error("优惠方案计算被中断，{}", e.getMessage());
        }

        // 所有组合都弄出来了
        log.info("所有组合都弄出来了");
        // 打印所有方案

        // 6.筛选最优解
        //- 用券相同时，优惠金额最高的方案
        //- 优惠金额相同时，用券最少的方案
        return findBestSolutions(list);

    }

    /**
     * 分页查询我的优惠券接口
     *
     * @param userCouponIds
     * @return
     */
    @Override
    public List<String> queryDiscountRules(List<Long> userCouponIds) {
        List<Coupon> coupons = baseMapper.queryCouponByUserCouponIds(userCouponIds, UserCouponStatus.USED);
        if (CollUtils.isEmpty(coupons)) {
            return CollUtils.emptyList();
        }
        // 2.转换规则
        return coupons.stream().map(c -> DiscountStrategy.getDiscount(c.getDiscountType()).getRule(c)).collect(Collectors.toList());
    }


    /**
     * 根据券方案计算订单优惠明细
     *
     * @param orderCouponDTO
     * @return
     */
    @Override
    public CouponDiscountDTO queryDiscountDetailByOrder(OrderCouponDTO orderCouponDTO) {
        // 1.查询用户优惠券
        List<Long> userCouponIds = orderCouponDTO.getUserCouponIds();
        List<Coupon> coupons = this.getBaseMapper().queryCouponByUserCouponIds(userCouponIds, UserCouponStatus.UNUSED);
        // 查询里面未使用的优惠券
        if (CollUtils.isEmpty(coupons)) {
            return null;
        }
        // List<OrderCourseDTO> orderCourses
        Map<Coupon, List<OrderCourseDTO>> availableCouponMap = findAvailableCoupons(coupons, orderCouponDTO.getCourseList());
        if (CollUtils.isEmpty(availableCouponMap)) {
            return null;
        }
        // 3.查询优惠券规则
        return calculateSolutionDiscount(availableCouponMap, orderCouponDTO.getCourseList(), coupons);
    }

    /**
     * 核销优惠券
     *
     * @param userCouponIds
     */
    @Transactional
    @Override
    public void writeOffCoupon(List<Long> userCouponIds) {
        // 1.查询优惠券
        List<UserCoupon> userCoupons = listByIds(userCouponIds);
        if (CollUtils.isEmpty(userCoupons)) {
            return;
        }
        // 2.处理数据
        List<UserCoupon> list = userCoupons.stream()
                // 过滤无效券
                .filter(coupon -> {
                    if (coupon == null) {
                        return false;
                    }
                    if (UserCouponStatus.UNUSED != coupon.getStatus()) {
                        return false;
                    }
                    LocalDateTime now = LocalDateTime.now();
                    return !now.isBefore(coupon.getTermBeginTime()) && !now.isAfter(coupon.getTermEndTime());
                })
                .map(coupon -> {
                    UserCoupon c = new UserCoupon();
                    c.setId(coupon.getId());
                    c.setStatus(UserCouponStatus.USED);
                    return c;
                })
                .collect(Collectors.toList());

        if (ObjectUtils.isEmpty(list)) {
            return;
        }

        // 4.核销，修改优惠券状态
        boolean success = updateBatchById(list);
        if (!success) {
            return;
        }

        // 5.更新已使用数量
        List<Long> couponIds = userCoupons.stream().map(UserCoupon::getCouponId).collect(Collectors.toList());

        // 先用mybatis-PLUS实现
        //Wrappers.<Coupon>lambdaUpdate().in(Coupon::getId, couponIds)
        //      .last("used_num = used_num + 1");

        int c = couponMapper.incrUsedNum(couponIds, 1);
        // 根据返回值
        if (c == 0) {
            // 说明没有更新成功
            // 事务回滚
            throw new BizIllegalException("优惠券核销失败");
        }


    }

    /**
     * 退还指定优惠券
     *
     * @param userCouponIds
     */
    @Transactional
    @Override
    public void refundCoupon(List<Long> userCouponIds) {
        // 1.查询优惠券
        List<UserCoupon> userCoupons = listByIds(userCouponIds);
        if (CollUtils.isEmpty(userCoupons)) {
            return;
        }
        // 2.处理优惠券数据
        List<UserCoupon> list = userCoupons.stream()
                // 过滤无效券
                .filter(coupon -> coupon != null && UserCouponStatus.USED == coupon.getStatus())
                // 更新状态字段
                .map(coupon -> {
                    UserCoupon c = new UserCoupon();
                    c.setId(coupon.getId());
                    // 3.判断有效期，是否已经过期，如果过期，则状态为 已过期，否则状态为 未使用
                    LocalDateTime now = LocalDateTime.now();
                    UserCouponStatus status = now.isAfter(coupon.getTermEndTime()) ?
                            UserCouponStatus.EXPIRED : UserCouponStatus.UNUSED;
                    c.setStatus(status);
                    return c;
                }).collect(Collectors.toList());

        if (ObjectUtils.isEmpty(list)) {
            return;
        }

        // 4.修改优惠券状态
        boolean success = updateBatchById(list);
        if (!success) {
            return;
        }
        // 5.更新已使用数量
        List<Long> couponIds = userCoupons.stream().map(UserCoupon::getCouponId).collect(Collectors.toList());
        int c = couponMapper.incrUsedNum(couponIds, -1);
        if (c < 1) {
            throw new DbException("更新优惠券使用数量失败！");
        }
    }


    /**
     * 筛选最优解
     * 弄成两个map
     * 一个key是优惠券id 那个value就是优惠的最大金额
     * 一个key是优惠券金额  那个value就是优惠券的数量
     *
     * @param list
     * @return
     */
    private List<CouponDiscountDTO> findBestSolutions(List<CouponDiscountDTO> list) {
        // 1.准备Map记录最优解
        Map<String, CouponDiscountDTO> moreDiscountMap = new HashMap<>();
        Map<Integer, CouponDiscountDTO> lessCouponMap = new HashMap<>();
        // 2.遍历，筛选最优解
        for (CouponDiscountDTO solution : list) {
            // 2.1.计算当前方案的id组合
            // 排好序后转为String 并且用逗号分隔
            String ids = solution.getIds().stream().sorted(Long::compareTo).map(String::valueOf).collect(Collectors.joining(","));
            // 2.2.比较用券相同时，优惠金额是否最大
            CouponDiscountDTO best = moreDiscountMap.get(ids);
            if (best != null && best.getDiscountAmount() >= solution.getDiscountAmount()) {
                // 当前方案优惠金额少，跳过
                continue;
            }
            // 2.3.比较优惠金额相同时，用券数量是否最少
            best = lessCouponMap.get(solution.getDiscountAmount());
            int newSize = solution.getIds().size();
            // newSize>1表示当前方案的优惠券数量大于1  也就是说单券不参与比较要进行返回
            // best.getIds().size()表示老的方案的优惠券数量
            if (newSize > 1 && best != null && best.getIds().size() <= newSize) {
                // 当前方案用券数量多，跳过
                continue;
            }
            // 那么就可以放入map
            moreDiscountMap.put(ids, solution);
            lessCouponMap.put(solution.getDiscountAmount(), solution);
        }
        // 3.求交集
        Collection<CouponDiscountDTO> bestSolutions = CollUtils.intersection(moreDiscountMap.values(), lessCouponMap.values());
        // 4.排序，按优惠金额降序
        return bestSolutions.stream().sorted(Comparator.comparingInt(CouponDiscountDTO::getDiscountAmount).reversed()).collect(Collectors.toList());
    }

    /**
     * 计算每一种方案的  优惠明细
     *
     * @param avaMap       优惠券和可用课程的映射关系
     * @param orderCourses 订单中的所有课程
     * @param solution     优惠券方案
     * @return
     */
    private CouponDiscountDTO calculateSolutionDiscount
    (Map<Coupon, List<OrderCourseDTO>> avaMap, List<OrderCourseDTO> orderCourses, List<Coupon> solution) {
        // 1.创建优惠明细对象  优惠券id集合  优惠券规则集合  优惠金额(默认是0)
        CouponDiscountDTO discountDTO = new CouponDiscountDTO();
        // 2.初始化商品id和商品折扣和明细地映射，  初始化折扣明细全部设置为0
        Map<Long, Integer> detailMap = orderCourses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, item -> 0));
        discountDTO.setDiscountDetail(detailMap);
        // 3.计算该方案的优惠信息
        for (Coupon coupon : solution) {
            // 3.1.获取该优惠券对应的可用课程
            List<OrderCourseDTO> availableCourses = avaMap.get(coupon);
            // 3.2. 计算可用课程的总金额  (商品价格-该商品的折扣明细)
            int totalAmount = availableCourses.stream().mapToInt(value -> value.getPrice() - detailMap.get(value.getId())).sum();
            // 3.3. 判断优惠券是否可用
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            boolean canUse = discount.canUse(totalAmount, coupon);
            if (!canUse) {
                // 3.4.如果不可用，则跳过该优惠券
                continue;
            }
            // 3.5.计算优惠金额 计算优惠券使用过的折扣值
            int discountAmount = discount.calculateDiscount(totalAmount, coupon);
            // 3.6.计算商品的折扣明细,更新到detailMap里面,更新商品的折扣明细(更新商品id的商品折扣明细)
            // 需要detailMap 和 折扣金额
            calculateDetailDiscount(detailMap, discountAmount, availableCourses, totalAmount);

            // 3.7 只要执行到当前这句话 就表示当前优惠券可用
            discountDTO.getIds().add(coupon.getId());
            discountDTO.getRules().add(discount.getRule(coupon));
            // 进行累加 不能进行覆盖  就是本方案最终的优惠券
            discountDTO.setDiscountAmount(discountAmount + discountDTO.getDiscountAmount());
        }
        return discountDTO;
    }

    /**
     * 计算商品的折扣明细
     *
     * @param detailMap        商品id和商品折扣明细的映射
     * @param discountAmount   当前优惠券优惠金额(折扣金额)
     * @param availableCourses 可用课程
     * @param totalAmount      可用课程的总金额(商品价格-该商品的折扣明细  也就是之后的)
     */
    private void calculateDetailDiscount(Map<Long, Integer> detailMap, int discountAmount, List<
            OrderCourseDTO> availableCourses, int totalAmount) {

        // 目的是为了计算商品的折扣明细
        // 规则:前面的商品按照比例计算，最后一个总的优惠金额减去前面的优惠金额
        int time = 0;
        int remainAmount = discountAmount; // 剩余的优惠金额
        // 循环所有的商品
        for (OrderCourseDTO availableCourse : availableCourses) {
            time++;
            int discount;
            if (time == availableCourses.size()) {
                // 最后一个商品
                // 商品的折扣明细 = 商品的价格 - 前面的商品的折扣明细
                discount = remainAmount;
            } else {
                // 计算商品的折扣明细
                // 商品的折扣明细 = 商品的价格 * 优惠金额 / 可用课程的总金额
                discount = availableCourse.getPrice() * discountAmount / totalAmount;
                remainAmount = remainAmount - discount;
            }
            // 这里进行累加
            detailMap.put(availableCourse.getId(), discount + detailMap.get(availableCourse.getId()));
        }

    }

    /**
     * 细筛 查询每一个优惠券与之对应的可用课程
     *
     * @param coupons
     * @param orderCourses
     * @return
     */
    private Map<Coupon, List<OrderCourseDTO>> findAvailableCoupons
    (List<Coupon> coupons, List<OrderCourseDTO> orderCourses) {
        Map<Coupon, List<OrderCourseDTO>> result = new HashMap<>(coupons.size());
        // 1.循环遍历初筛后的优惠券集合
        for (Coupon coupon : coupons) {
            // 2.找出每一个优惠券的可用的课程
            // 如果没有限制课程，则所有课程都可用
            List<OrderCourseDTO> availableCourses = orderCourses;
            // 2.1.判断是否有限定课程
            if (coupon.getSpecific()) {
                // 2.2.查询限定范围  查询coupon_scope  根据couponId
                List<CouponScope> scopeList = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();

                // 没有分类的话
                if (!CollUtils.isEmpty(scopeList)) {
                    // 2.3.得到限定课程的id集合
                    // 这个是肯定不为null的  bizId就表示分类id  但是我们还是要判断一下
                    Set<Long> scopeIds = scopeList.stream().map(CouponScope::getBizId).collect(Collectors.toSet());

                    // 2.4.筛选可用课程
                    availableCourses = orderCourses.stream().filter(item -> scopeIds.contains(item.getCateId())).collect(Collectors.toList());
                }

            }

            // 没有可用课程  说明当前优惠券限定了课程，但是订单中没有这些课程，说明改优惠券不可用
            if (CollUtils.isEmpty(availableCourses)) {
                continue;
            }

            // 3.计算概优惠券  可用课程的总金额
            int totalAmount = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();

            // 4.判断概优惠券是否可用
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if (discount.canUse(totalAmount, coupon)) {
                // 5.如果可用，则将优惠券与可用课程放入map中
                result.put(coupon, availableCourses);
            }
        }
        return result;
    }

    /**
     * 保存用户券
     *
     * @param coupon
     * @param userId
     */
    private void saveUserCoupon(Coupon coupon, Long userId) {

        // 1.基本信息
        UserCoupon uc = new UserCoupon();
        uc.setUserId(userId);
        uc.setCouponId(coupon.getId());
        // 2.有效期信息
        LocalDateTime termBeginTime = coupon.getTermBeginTime();
        LocalDateTime termEndTime = coupon.getTermEndTime();
        if (termBeginTime == null && termEndTime == null) {
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        uc.setTermBeginTime(termBeginTime);
        uc.setTermEndTime(termEndTime);
        // 3.保存
        save(uc);
    }
}
